﻿namespace Webinator.Impls.WatiNImpl
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Text.RegularExpressions;
    using System.Threading;
    using Enums;
    using Interfaces;
    using Toolboxes;
    using WatiN.Core;
    using WatiN.Core.Constraints;

    [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Reviewed. Suppression is OK here.")]
    internal class WatiNCore : ICore
    {
        private const int DefaultPollRate = 250;

        private const int DefaultTimeout = 20000;

        public Browser Browser;

        private CoreCommon _common;

        public WatiNCore(Browser browser, Config config)
        {
            // When the browser changes, update it via UpdateCore()
            Browser = browser;
            _common = new CoreCommon();
            Config = config;
        }

        public Config Config { get; set; }

        public void AddCookie(string name, string value, string path, string domain)
        {
            // WatiN doesn't support any native methods here
            throw new NotImplementedException();
        }

        public void CloseBrowser()
        {
            Browser.Close();
        }

        public void ConfirmDialog()
        {
            throw new NotImplementedException();
        }

        public bool DialogPresent()
        {
            return false;
        }

        public void DeleteCookie(string name)
        {
            // WatiN doesn't support any native methods here
            throw new NotImplementedException();
        }

        public void DismissDialog()
        {
            throw new NotImplementedException();
        }

        public void ElementClear(object element)
        {
            TextField textField = (TextField)element;
            CoreCommon.SafeAction(textField, () => textField.TypeText(string.Empty));
        }

        public void ElementClick(object element)
        {
            if (!(element is Element)) throw new Exception("Element found is not the correct type.");
            var e = (Element)element;
            CoreCommon.SafeAction(e, e.Click);
        }

        public void ElementFocus(object element)
        {
            if (!(element is Element)) throw new Exception("Element found is not the correct type.");
            CoreCommon.SafeAction(element, () => ((Element)element).Focus());
        }

        public bool ElementIsChecked(object element)
        {
            if (!(element is CheckBox) && !(element is RadioButton)) throw new Exception("Element found is not the correct type.");

            var checkBox = element as CheckBox;
            if (checkBox != null)
            {
                return checkBox.Checked;
            }

            var radioButton = element as RadioButton;
            return radioButton.Checked;
        }

        public bool ElementIsDisplayed(object element)
        {
            // No native method exists for this in WatiN
            if (!(element is Element)) throw new Exception("Element found is not the correct type.");

            InjectJquery();

            /* Checks for display:[none|block], ignores visible:[true|false]
             * It's not perfect, but if it's an issue we can refine it later
             */
            var js = string.Format(@"$({0}).is("":visible"")", ((Element)element).GetJavascriptElementReference());
            return Convert.ToBoolean(EvaluateScript(js));
        }

        public void ElementSelect(object element)
        {
            if (!(element is CheckBox) && !(element is RadioButton)) throw new Exception("Element found is not the correct type.");
            if (element is CheckBox)
            {
                CoreCommon.SafeAction(element, ((Element)element).Click);
            }

            if (element is RadioButton)
            {
                CoreCommon.SafeAction(element, ((Element)element).Click);
            }
        }

        public void ElementSendkeys(object element, string text)
        {
            ((Element)element).Focus();
            if (!(element is TextField)) throw new Exception("Element found is not the correct type.");
            TextField textField = (TextField)element;
            CoreCommon.SafeAction(textField, () => textField.TypeText(text));
        }

        public void ElementTriggerEvent(object element, EventIs eventIs)
        {
            // Native FireEvent() method isn't quite as flexible or powerful...
            var myEvent = eventIs.ToString().ToLower();
            InjectJquery();

            var e = (Element)element;
            var js = eventIs == EventIs.RightClick
                ? string.Format("$({0}).trigger({{type: 'mousedown', which: 3}});", e.GetJavascriptElementReference())
                : string.Format("$({0}).trigger('{1}');", e.GetJavascriptElementReference(), myEvent);

            Browser.RunScript(js);
        }

        public object EvaluateScript(string js)
        {
            if (Config.Browser == Config.AvailableBrowsers.Firefox)
                js = CleanUpJsCommandForFirefox(js);

            if (js.StartsWith("return"))
                js = js.Replace("return", string.Empty).Trim();

            try
            {
                return Browser.Eval(js);
            }
            catch (Exception e)
            {
                throw new Exception("Unable to run the javascript command: " + js + "\nError : " + e);
            }
        }

        public void ExecuteScript(string js)
        {
            if (Config.Browser == Config.AvailableBrowsers.Firefox)
                js = CleanUpJsCommandForFirefox(js);

            try
            {
                Browser.RunScript(js);
            }
            catch (Exception e)
            {
                throw new Exception("Unable to run the javascript command: " + js + "\nError : " + e);
            }
        }

        public object FindElement(Locator locator, int timeout = DefaultTimeout)
        {
            if (locator.FindBy == FindBy.XPath || locator.FindBy == FindBy.Css)
                return FindByXPath(locator.Value, locator.Index);

            AttributeConstraint findBy = GetFindByFromLocator(locator);
            Element element = WaitForElement(findBy, locator.Index, timeout);
            if (element == null || element.Exists == false)
            {
                throw new Exception("Cannot find element at: " + findBy);
            }

            return element;
        }

        public object FindElements(Locator locator)
        {
            if (locator.FindBy == FindBy.XPath || locator.FindBy == FindBy.Css)
                return FindByXPath(locator.Value);

            AttributeConstraint findBy = GetFindByFromLocator(locator);
            ElementCollection elements = FindElementsBy(findBy);
            if (elements == null || elements.Count == 0)
            {
                throw new Exception("Cannot find elements at: " + findBy);
            }

            return elements;
        }

        public string GetAttribute(object element, string attribute)
        {
            var e = (Element)element;
            return CoreCommon.SafeAction(e, () => e.GetAttributeValue(attribute)) ?? string.Empty;
        }

        public object GetCore()
        {
            return Browser;
        }

        public string GetElementText(object element)
        {
            if (!(element is Element)) throw new Exception("Element found is not the correct type.");
            return ((Element)element).Text;
        }

        public IEnumerable<Rectangle> GetLocatorLocations(Locator locator)
        {
            List<Rectangle> locations = new List<Rectangle>();
            string javascript = @"function getRect(){{var r={0}.getBoundingClientRect();var result = r.left + ' ' + r.top + ' ' + r.width + ' ' + r.height;return result;}}getRect();";

            var elements = (ElementCollection)FindElements(locator);
            foreach (Element element in elements)
            {
                var obj = (string)EvaluateScript(string.Format(javascript, element.GetJavascriptElementReference()));
                var vals = obj.Split(' ');
                var location = new Rectangle(
                    (int)double.Parse(vals[0]),
                    (int)double.Parse(vals[1]),
                    (int)double.Parse(vals[2]),
                    (int)double.Parse(vals[3]));
                locations.Add(location);
            }

            return locations;
        }

        public Rectangle GetLocatorLocation(Locator locator)
        {
            string javascript = @"function getRect(){{var r={0}.getBoundingClientRect();var result = r.left + ' ' + r.top + ' ' + r.width + ' ' + r.height;return result;}}getRect();";

            var element = (Element)FindElement(locator);
            var obj = (string)EvaluateScript(string.Format(javascript, element.GetJavascriptElementReference()));
            var vals = obj.Split(' ');
            var location = new Rectangle(
                (int)double.Parse(vals[0]),
                (int)double.Parse(vals[1]),
                (int)double.Parse(vals[2]),
                (int)double.Parse(vals[3]));

            return location;
        }

        public string GetPageSource()
        {
            string html = null;
            int i = 0;
            bool success;
            do
            {
                try
                {
                    html = Browser.Html;
                    success = true;
                }
                catch (InvalidOperationException)
                {
                    success = false;
                }

                if (!success)
                {
                    Thread.Sleep(100);
                }

                i++;
            }
            while (!success && i < 5);

            return html;
        }

        public string GetPageTitle()
        {
            return Browser.Title;
        }

        public string GetPageUrl()
        {
            return Browser.Url;
        }

        public SelectedItem GetSelectedItem(Locator locator)
        {
            var element = FindElement(locator);
            if (!(element is SelectList)) throw new Exception("Element found is not the correct type.");
            var e = (SelectList)element;

            var selectedItem = new SelectedItem
            {
                Index = e.SelectedOption.Index,
                Text = e.SelectedOption.Text,
                Value = e.SelectedOption.Value,
            };

            return selectedItem;
        }

        public IEnumerable<SelectedItem> GetSelectedItems(Locator locator)
        {
            var element = FindElement(locator);
            if (!(element is SelectList)) throw new Exception("Element found is not the correct type.");
            var e = (SelectList)element;

            List<SelectedItem> selectedItems = new List<SelectedItem>();
            foreach (var option in e.Options)
            {
                if (option.Selected)
                {
                    var selectedItem = new SelectedItem
                    {
                        Index = option.Index,
                        Text = option.Text,
                        Value = option.Value,
                    };

                    selectedItems.Add(selectedItem);
                }
            }

            return selectedItems;
        }

        public string GetVisibleText()
        {
            return Browser.Text;
        }

        public void NavigateBack()
        {
            Browser.Back();
        }

        public void NavigateForward()
        {
            Browser.Forward();
        }

        public void NavigateRefresh()
        {
            Browser.Refresh();
        }

        public void NavigateToUrl(string url)
        {
            Browser.GoTo(url);
        }

        public bool QuickElementExists(Locator locator)
        {
            var compiledXpath = XPathToolbox.GetXpathFromLocator(locator);
            return _common.HtmlContainsXpath(compiledXpath, GetPageSource());
        }

        public bool QuickWaitElementDoesntExist(Locator locator, int waitInMilliseconds)
        {
            return _common.QuickWait(locator, waitInMilliseconds, false, GetPageSource);
        }

        public bool QuickWaitElementExists(Locator locator, int waitInMilliseconds)
        {
            return _common.QuickWait(locator, waitInMilliseconds, true, GetPageSource);
        }

        public void SelectListItems(Locator locator, Selector[] selectors)
        {
            var element = FindElement(locator);
            if (!(element is SelectList)) throw new Exception("Element found is not the correct type.");
            var e = (SelectList)element;

            if (selectors.Count() == 1)
            {
                var selector = selectors.First();
                switch (selector.SelectorIs)
                {
                    case SelectorIs.Text:
                        e.Select(selector.Argument);
                        break;
                    case SelectorIs.PartialText:
                        e.Select(new Regex(selector.Argument));
                        break;
                    case SelectorIs.Value:
                        e.SelectByValue(selector.Argument);
                        break;
                    case SelectorIs.PartialValue:
                        e.SelectByValue(new Regex(selector.Argument));
                        break;
                    case SelectorIs.Index:
                        e.Options[int.Parse(selector.Argument)].Select();
                        break;
                }
            }
            else
            {
                // Yikes! Too much nesting, feel free to make this better.
                foreach (var selector in selectors)
                {
                    foreach (var option in e.Options)
                    {
                        switch (selector.SelectorIs)
                        {
                            case SelectorIs.Text:
                                if (option.Text == selector.Argument) option.Select();
                                break;
                            case SelectorIs.PartialText:
                                if (option.Text.Contains(selector.Argument)) option.Select();
                                break;
                            case SelectorIs.Value:
                                if (option.Value == selector.Argument) option.Select();
                                break;
                            case SelectorIs.PartialValue:
                                if (option.Value.Contains(selector.Argument)) option.Select();
                                break;
                            case SelectorIs.Index:
                                if (option.Index == int.Parse(selector.Argument)) option.Select();
                                break;
                        }
                    }
                }
            }
        }

        public void SetAttribute(object element, string attribute, string value)
        {
            var e = (Element)element;
            CoreCommon.SafeAction(e, () => e.SetAttributeValue(attribute, value));
        }

        public void UpdateCore(object driver)
        {
            Browser = (Browser)driver;
        }

        public bool WaitElementAppear(Locator locator, int waitInMilliseconds, int index = 0)
        {
            return WaitForElement(GetFindByFromLocator(locator), index, waitInMilliseconds) != null;
        }

        public bool WaitElementDisappear(Locator locator, int waitInMilliseconds, int index = 0)
        {
            var y = waitInMilliseconds / DefaultPollRate;

            for (var x = 0; x < y; x++)
            {
                if (!WaitElementAppear(locator, DefaultPollRate, index))
                {
                    return true;
                }

                Thread.Sleep(DefaultPollRate);
            }

            return false;
        }

        public void SwitchWindow(SwitchBy switchBy, Comparison comparison, string argument)
        {
            throw new NotImplementedException();
        }

        public void SwitchToMainWindow()
        {
            throw new NotImplementedException();
        }

        public void SwitchToMainFrame()
        {
            throw new NotImplementedException();
        }

        public void SwitchToFrame(Locator locator)
        {
            throw new NotImplementedException();
        }

        public bool CurrentWindowIsValid()
        {
            throw new NotImplementedException();
        }

        public void CloseWindow()
        {
            throw new NotImplementedException();
        }

        public IEnumerable<string> WindowTitles()
        {
            throw new NotImplementedException();
        }

        public IEnumerable<string> WindowUrls()
        {
            throw new NotImplementedException();
        }

        private string CleanUpJsCommandForFirefox(string jsCommand)
        {
            return jsCommand.Replace("$", "window.$").Replace("document", "doc");
        }

        private Element FindByXPath(string xpath, int index)
        {
            InjectJavascriptXpath();

            // Set unique ID or return existing ID
            string uniqueId = Randomator.RandomAlphanumeric(20);
            var js = new StringBuilder();
            js.Append(string.Format(@"function returnId(){{var result = document.evaluate({0}, document, null, 7, null);", XPathToolbox.XPathLiteral(xpath)));
            js.Append(string.Format(@"var e = result.snapshotItem({0});", index));
            js.Append(@"if (e.id.length > 0) {return e.id;} else {");
            js.Append(string.Format(@"e.setAttribute('id', '{0}'); return '{0}'; }}}}returnId();", uniqueId));
            uniqueId = (string)EvaluateScript(js.ToString());

            // Get element
            var e = Browser.Elements.Filter(Find.ById(uniqueId))[0];

            return e;
        }

        private ElementCollection FindByXPath(string xpath)
        {
            InjectJavascriptXpath();

            // Set unique names
            string uniqueName = Randomator.RandomAlphanumeric(20);
            var js = new StringBuilder();
            js.Append(@"var result = document.evaluate(" + XPathToolbox.XPathLiteral(xpath) + ", document, null, 5, null);");
            js.Append(@"var n=result.iterateNext();try{while (n){");
            js.Append(@"n.setAttribute('oldName', e.getAttribute('name'));");
            js.Append(@"n.setAttribute('name', '" + uniqueName + "');");
            js.Append(@"n=result.iterateNext();}}catch(e){}");
            Browser.RunScript(js.ToString());

            // Get element
            ElementCollection list = Browser.Elements.Filter(Find.ByName(uniqueName));

            // Restore old names
            js.Append(@"var result = document.evaluate(" + XPathToolbox.XPathLiteral(xpath) + ", document, null, 5, null);");
            js.Append(@"var n=result.iterateNext();try{while (n){");
            js.Append(@"n.setAttribute('name', e.getAttribute('oldName'));");
            js.Append(@"n=result.iterateNext();}}catch(e){}");

            return list;
        }

        private ElementCollection FindElementsBy(AttributeConstraint findBy)
        {
            ElementCollection list = Browser.Elements.Filter(findBy);

            if (list == null || list.Count == 0)
            {
                return null;
            }

            return list;
        }

        private AttributeConstraint GetFindByFromLocator(Locator locator)
        {
            switch (locator.FindBy)
            {
                case FindBy.Id:
                    return locator.IsPartial
                               ? Find.ById(new Regex(locator.Value))
                               : Find.ById(locator.Value);
                case FindBy.Name:
                    return locator.IsPartial
                               ? Find.ByName(new Regex(locator.Value))
                               : Find.ByName(locator.Value);
                case FindBy.Text:
                    return locator.IsPartial
                               ? Find.ByText(new Regex(locator.Value))
                               : Find.ByText(locator.Value);
                case FindBy.Class:
                    return locator.IsPartial
                               ? Find.ByClass(new Regex(locator.Value))
                               : Find.ByClass(locator.Value);
                case FindBy.Value:
                    return locator.IsPartial
                               ? Find.ByValue(new Regex(locator.Value))
                               : Find.ByValue(locator.Value);
                case FindBy.Href:
                    return locator.IsPartial
                               ? (AttributeConstraint)Find.ByUrl(new Regex(locator.Value))
                               : Find.ByUrl(locator.Value);
                case FindBy.Alt:
                    return locator.IsPartial
                               ? Find.ByAlt(new Regex(locator.Value))
                               : Find.ByAlt(locator.Value);
                case FindBy.Css:
                case FindBy.XPath:
                    throw new Exception("Not implemented.");
                default:
                    throw new Exception("No Find(By) Constraint for : " + locator.FindBy);
            }
        }

        private void InjectJavascriptXpath()
        {
            try
            {
                Browser.RunScript(Scripts.JavascriptXPath);
            }
            catch (Exception)
            {
                /* Swallow error
                 */
            }
        }

        private void InjectJquery()
        {
            ExecuteScript(Scripts.JQuery);
        }

        private Element WaitForElement(AttributeConstraint findBy, int index = 0, int timeout = DefaultTimeout)
        {
            ElementCollection list = Browser.Elements.Filter(findBy);
            Element element = list[index];
            element.WaitUntilExists(timeout);

            if (!element.Exists)
            {
                return null;
            }

            return element;
        }
    }
}